/*
 * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.lang.invoke;

import jdk.internal.org.objectweb.asm.*;
import sun.invoke.util.BytecodeDescriptor;
import sun.misc.Unsafe;
import sun.security.action.GetPropertyAction;

import java.io.FilePermission;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.LinkedHashSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.PropertyPermission;
import java.util.Set;

import static jdk.internal.org.objectweb.asm.Opcodes.*;

/**
 * Lambda metafactory implementation which dynamically creates an
 * inner-class-like class per lambda callsite.
 *
 * @see LambdaMetafactory
 */
/* package */ final class InnerClassLambdaMetafactory extends AbstractValidatingLambdaMetafactory {

  private static final Unsafe UNSAFE = Unsafe.getUnsafe();

  private static final int CLASSFILE_VERSION = 52;
  private static final String METHOD_DESCRIPTOR_VOID = Type.getMethodDescriptor(Type.VOID_TYPE);
  private static final String JAVA_LANG_OBJECT = "java/lang/Object";
  private static final String NAME_CTOR = "<init>";
  private static final String NAME_FACTORY = "get$Lambda";

  //Serialization support
  private static final String NAME_SERIALIZED_LAMBDA = "java/lang/invoke/SerializedLambda";
  private static final String NAME_NOT_SERIALIZABLE_EXCEPTION = "java/io/NotSerializableException";
  private static final String DESCR_METHOD_WRITE_REPLACE = "()Ljava/lang/Object;";
  private static final String DESCR_METHOD_WRITE_OBJECT = "(Ljava/io/ObjectOutputStream;)V";
  private static final String DESCR_METHOD_READ_OBJECT = "(Ljava/io/ObjectInputStream;)V";
  private static final String NAME_METHOD_WRITE_REPLACE = "writeReplace";
  private static final String NAME_METHOD_READ_OBJECT = "readObject";
  private static final String NAME_METHOD_WRITE_OBJECT = "writeObject";
  private static final String DESCR_CTOR_SERIALIZED_LAMBDA
      = MethodType.methodType(void.class,
      Class.class,
      String.class, String.class, String.class,
      int.class, String.class, String.class, String.class,
      String.class,
      Object[].class).toMethodDescriptorString();
  private static final String DESCR_CTOR_NOT_SERIALIZABLE_EXCEPTION
      = MethodType.methodType(void.class, String.class).toMethodDescriptorString();
  private static final String[] SER_HOSTILE_EXCEPTIONS = new String[]{
      NAME_NOT_SERIALIZABLE_EXCEPTION};


  private static final String[] EMPTY_STRING_ARRAY = new String[0];

  // Used to ensure that each spun class name is unique
  private static final AtomicInteger counter = new AtomicInteger(0);

  // For dumping generated classes to disk, for debugging purposes
  private static final ProxyClassesDumper dumper;

  static {
    final String key = "jdk.internal.lambda.dumpProxyClasses";
    String path = AccessController.doPrivileged(
        new GetPropertyAction(key), null,
        new PropertyPermission(key, "read"));
    dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path);
  }

  // See context values in AbstractValidatingLambdaMetafactory
  private final String implMethodClassName;        // Name of type containing implementation "CC"
  private final String implMethodName;             // Name of implementation method "impl"
  private final String implMethodDesc;             // Type descriptor for implementation methods "(I)Ljava/lang/String;"
  private final Class<?> implMethodReturnClass;    // class for implementaion method return type "Ljava/lang/String;"
  private final MethodType constructorType;        // Generated class constructor type "(CC)void"
  private final ClassWriter cw;                    // ASM class writer
  private final String[] argNames;                 // Generated names for the constructor arguments
  private final String[] argDescs;                 // Type descriptors for the constructor arguments
  private final String lambdaClassName;            // Generated name for the generated class "X$$Lambda$1"

  /**
   * General meta-factory constructor, supporting both standard cases and
   * allowing for uncommon options such as serialization or bridging.
   *
   * @param caller Stacked automatically by VM; represents a lookup context with the accessibility
   * privileges of the caller.
   * @param invokedType Stacked automatically by VM; the signature of the invoked method, which
   * includes the expected static type of the returned lambda object, and the static types of the
   * captured arguments for the lambda.  In the event that the implementation method is an instance
   * method, the first argument in the invocation signature will correspond to the receiver.
   * @param samMethodName Name of the method in the functional interface to which the lambda or
   * method reference is being converted, represented as a String.
   * @param samMethodType Type of the method in the functional interface to which the lambda or
   * method reference is being converted, represented as a MethodType.
   * @param implMethod The implementation method which should be called (with suitable adaptation of
   * argument types, return types, and adjustment for captured arguments) when methods of the
   * resulting functional interface instance are invoked.
   * @param instantiatedMethodType The signature of the primary functional interface method after
   * type variables are substituted with their instantiation from the capture site
   * @param isSerializable Should the lambda be made serializable?  If set, either the target type
   * or one of the additional SAM types must extend {@code Serializable}.
   * @param markerInterfaces Additional interfaces which the lambda object should implement.
   * @param additionalBridges Method types for additional signatures to be bridged to the
   * implementation method
   * @throws LambdaConversionException If any of the meta-factory protocol invariants are violated
   */
  public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,
      MethodType invokedType,
      String samMethodName,
      MethodType samMethodType,
      MethodHandle implMethod,
      MethodType instantiatedMethodType,
      boolean isSerializable,
      Class<?>[] markerInterfaces,
      MethodType[] additionalBridges)
      throws LambdaConversionException {
    super(caller, invokedType, samMethodName, samMethodType,
        implMethod, instantiatedMethodType,
        isSerializable, markerInterfaces, additionalBridges);
    implMethodClassName = implDefiningClass.getName().replace('.', '/');
    implMethodName = implInfo.getName();
    implMethodDesc = implMethodType.toMethodDescriptorString();
    implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)
        ? implDefiningClass
        : implMethodType.returnType();
    constructorType = invokedType.changeReturnType(Void.TYPE);
    lambdaClassName =
        targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
    cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    int parameterCount = invokedType.parameterCount();
    if (parameterCount > 0) {
      argNames = new String[parameterCount];
      argDescs = new String[parameterCount];
      for (int i = 0; i < parameterCount; i++) {
        argNames[i] = "arg$" + (i + 1);
        argDescs[i] = BytecodeDescriptor.unparse(invokedType.parameterType(i));
      }
    } else {
      argNames = argDescs = EMPTY_STRING_ARRAY;
    }
  }

  /**
   * Build the CallSite. Generate a class file which implements the functional
   * interface, define the class, if there are no parameters create an instance
   * of the class which the CallSite will return, otherwise, generate handles
   * which will call the class' constructor.
   *
   * @return a CallSite, which, when invoked, will return an instance of the functional interface
   * @throws LambdaConversionException If properly formed functional interface is not found
   */
  @Override
  CallSite buildCallSite() throws LambdaConversionException {
    final Class<?> innerClass = spinInnerClass();
    if (invokedType.parameterCount() == 0) {
      final Constructor<?>[] ctrs = AccessController.doPrivileged(
          new PrivilegedAction<Constructor<?>[]>() {
            @Override
            public Constructor<?>[] run() {
              Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();
              if (ctrs.length == 1) {
                // The lambda implementing inner class constructor is private, set
                // it accessible (by us) before creating the constant sole instance
                ctrs[0].setAccessible(true);
              }
              return ctrs;
            }
          });
      if (ctrs.length != 1) {
        throw new LambdaConversionException("Expected one lambda constructor for "
            + innerClass.getCanonicalName() + ", got " + ctrs.length);
      }

      try {
        Object inst = ctrs[0].newInstance();
        return new ConstantCallSite(MethodHandles.constant(samBase, inst));
      } catch (ReflectiveOperationException e) {
        throw new LambdaConversionException("Exception instantiating lambda object", e);
      }
    } else {
      try {
        UNSAFE.ensureClassInitialized(innerClass);
        return new ConstantCallSite(
            MethodHandles.Lookup.IMPL_LOOKUP
                .findStatic(innerClass, NAME_FACTORY, invokedType));
      } catch (ReflectiveOperationException e) {
        throw new LambdaConversionException("Exception finding constructor", e);
      }
    }
  }

  /**
   * Generate a class file which implements the functional
   * interface, define and return the class.
   *
   * @return a Class which implements the functional interface
   * @throws LambdaConversionException If properly formed functional interface is not found
   * @implNote The class that is generated does not include signature information for exceptions
   * that may be present on the SAM method. This is to reduce classfile size, and is harmless as
   * checked exceptions are erased anyway, no one will ever compile against this classfile, and we
   * make no guarantees about the reflective properties of lambda objects.
   */
  private Class<?> spinInnerClass() throws LambdaConversionException {
    String[] interfaces;
    String samIntf = samBase.getName().replace('.', '/');
    boolean accidentallySerializable =
        !isSerializable && Serializable.class.isAssignableFrom(samBase);
    if (markerInterfaces.length == 0) {
      interfaces = new String[]{samIntf};
    } else {
      // Assure no duplicate interfaces (ClassFormatError)
      Set<String> itfs = new LinkedHashSet<>(markerInterfaces.length + 1);
      itfs.add(samIntf);
      for (Class<?> markerInterface : markerInterfaces) {
        itfs.add(markerInterface.getName().replace('.', '/'));
        accidentallySerializable |=
            !isSerializable && Serializable.class.isAssignableFrom(markerInterface);
      }
      interfaces = itfs.toArray(new String[itfs.size()]);
    }

    cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,
        lambdaClassName, null,
        JAVA_LANG_OBJECT, interfaces);

    // Generate final fields to be filled in by constructor
    for (int i = 0; i < argDescs.length; i++) {
      FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL,
          argNames[i],
          argDescs[i],
          null, null);
      fv.visitEnd();
    }

    generateConstructor();

    if (invokedType.parameterCount() != 0) {
      generateFactory();
    }

    // Forward the SAM method
    MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, samMethodName,
        samMethodType.toMethodDescriptorString(), null, null);
    mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
    new ForwardingMethodGenerator(mv).generate(samMethodType);

    // Forward the bridges
    if (additionalBridges != null) {
      for (MethodType mt : additionalBridges) {
        mv = cw.visitMethod(ACC_PUBLIC | ACC_BRIDGE, samMethodName,
            mt.toMethodDescriptorString(), null, null);
        mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
        new ForwardingMethodGenerator(mv).generate(mt);
      }
    }

    if (isSerializable) {
      generateSerializationFriendlyMethods();
    } else if (accidentallySerializable) {
      generateSerializationHostileMethods();
    }

    cw.visitEnd();

    // Define the generated class in this VM.

    final byte[] classBytes = cw.toByteArray();

    // If requested, dump out to a file for debugging purposes
    if (dumper != null) {
      AccessController.doPrivileged(new PrivilegedAction<Void>() {
                                      @Override
                                      public Void run() {
                                        dumper.dumpClass(lambdaClassName, classBytes);
                                        return null;
                                      }
                                    }, null,
          new FilePermission("<<ALL FILES>>", "read, write"),
          // createDirectories may need it
          new PropertyPermission("user.dir", "read"));
    }

    return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
  }

  /**
   * Generate the factory method for the class
   */
  private void generateFactory() {
    MethodVisitor m = cw
        .visitMethod(ACC_PRIVATE | ACC_STATIC, NAME_FACTORY, invokedType.toMethodDescriptorString(),
            null, null);
    m.visitCode();
    m.visitTypeInsn(NEW, lambdaClassName);
    m.visitInsn(Opcodes.DUP);
    int parameterCount = invokedType.parameterCount();
    for (int typeIndex = 0, varIndex = 0; typeIndex < parameterCount; typeIndex++) {
      Class<?> argType = invokedType.parameterType(typeIndex);
      m.visitVarInsn(getLoadOpcode(argType), varIndex);
      varIndex += getParameterSize(argType);
    }
    m.visitMethodInsn(INVOKESPECIAL, lambdaClassName, NAME_CTOR,
        constructorType.toMethodDescriptorString(), false);
    m.visitInsn(ARETURN);
    m.visitMaxs(-1, -1);
    m.visitEnd();
  }

  /**
   * Generate the constructor for the class
   */
  private void generateConstructor() {
    // Generate constructor
    MethodVisitor ctor = cw.visitMethod(ACC_PRIVATE, NAME_CTOR,
        constructorType.toMethodDescriptorString(), null, null);
    ctor.visitCode();
    ctor.visitVarInsn(ALOAD, 0);
    ctor.visitMethodInsn(INVOKESPECIAL, JAVA_LANG_OBJECT, NAME_CTOR,
        METHOD_DESCRIPTOR_VOID, false);
    int parameterCount = invokedType.parameterCount();
    for (int i = 0, lvIndex = 0; i < parameterCount; i++) {
      ctor.visitVarInsn(ALOAD, 0);
      Class<?> argType = invokedType.parameterType(i);
      ctor.visitVarInsn(getLoadOpcode(argType), lvIndex + 1);
      lvIndex += getParameterSize(argType);
      ctor.visitFieldInsn(PUTFIELD, lambdaClassName, argNames[i], argDescs[i]);
    }
    ctor.visitInsn(RETURN);
    // Maxs computed by ClassWriter.COMPUTE_MAXS, these arguments ignored
    ctor.visitMaxs(-1, -1);
    ctor.visitEnd();
  }

  /**
   * Generate a writeReplace method that supports serialization
   */
  private void generateSerializationFriendlyMethods() {
    TypeConvertingMethodAdapter mv
        = new TypeConvertingMethodAdapter(
        cw.visitMethod(ACC_PRIVATE + ACC_FINAL,
            NAME_METHOD_WRITE_REPLACE, DESCR_METHOD_WRITE_REPLACE,
            null, null));

    mv.visitCode();
    mv.visitTypeInsn(NEW, NAME_SERIALIZED_LAMBDA);
    mv.visitInsn(DUP);
    mv.visitLdcInsn(Type.getType(targetClass));
    mv.visitLdcInsn(invokedType.returnType().getName().replace('.', '/'));
    mv.visitLdcInsn(samMethodName);
    mv.visitLdcInsn(samMethodType.toMethodDescriptorString());
    mv.visitLdcInsn(implInfo.getReferenceKind());
    mv.visitLdcInsn(implInfo.getDeclaringClass().getName().replace('.', '/'));
    mv.visitLdcInsn(implInfo.getName());
    mv.visitLdcInsn(implInfo.getMethodType().toMethodDescriptorString());
    mv.visitLdcInsn(instantiatedMethodType.toMethodDescriptorString());
    mv.iconst(argDescs.length);
    mv.visitTypeInsn(ANEWARRAY, JAVA_LANG_OBJECT);
    for (int i = 0; i < argDescs.length; i++) {
      mv.visitInsn(DUP);
      mv.iconst(i);
      mv.visitVarInsn(ALOAD, 0);
      mv.visitFieldInsn(GETFIELD, lambdaClassName, argNames[i], argDescs[i]);
      mv.boxIfTypePrimitive(Type.getType(argDescs[i]));
      mv.visitInsn(AASTORE);
    }
    mv.visitMethodInsn(INVOKESPECIAL, NAME_SERIALIZED_LAMBDA, NAME_CTOR,
        DESCR_CTOR_SERIALIZED_LAMBDA, false);
    mv.visitInsn(ARETURN);
    // Maxs computed by ClassWriter.COMPUTE_MAXS, these arguments ignored
    mv.visitMaxs(-1, -1);
    mv.visitEnd();
  }

  /**
   * Generate a readObject/writeObject method that is hostile to serialization
   */
  private void generateSerializationHostileMethods() {
    MethodVisitor mv = cw.visitMethod(ACC_PRIVATE + ACC_FINAL,
        NAME_METHOD_WRITE_OBJECT, DESCR_METHOD_WRITE_OBJECT,
        null, SER_HOSTILE_EXCEPTIONS);
    mv.visitCode();
    mv.visitTypeInsn(NEW, NAME_NOT_SERIALIZABLE_EXCEPTION);
    mv.visitInsn(DUP);
    mv.visitLdcInsn("Non-serializable lambda");
    mv.visitMethodInsn(INVOKESPECIAL, NAME_NOT_SERIALIZABLE_EXCEPTION, NAME_CTOR,
        DESCR_CTOR_NOT_SERIALIZABLE_EXCEPTION, false);
    mv.visitInsn(ATHROW);
    mv.visitMaxs(-1, -1);
    mv.visitEnd();

    mv = cw.visitMethod(ACC_PRIVATE + ACC_FINAL,
        NAME_METHOD_READ_OBJECT, DESCR_METHOD_READ_OBJECT,
        null, SER_HOSTILE_EXCEPTIONS);
    mv.visitCode();
    mv.visitTypeInsn(NEW, NAME_NOT_SERIALIZABLE_EXCEPTION);
    mv.visitInsn(DUP);
    mv.visitLdcInsn("Non-serializable lambda");
    mv.visitMethodInsn(INVOKESPECIAL, NAME_NOT_SERIALIZABLE_EXCEPTION, NAME_CTOR,
        DESCR_CTOR_NOT_SERIALIZABLE_EXCEPTION, false);
    mv.visitInsn(ATHROW);
    mv.visitMaxs(-1, -1);
    mv.visitEnd();
  }

  /**
   * This class generates a method body which calls the lambda implementation
   * method, converting arguments, as needed.
   */
  private class ForwardingMethodGenerator extends TypeConvertingMethodAdapter {

    ForwardingMethodGenerator(MethodVisitor mv) {
      super(mv);
    }

    void generate(MethodType methodType) {
      visitCode();

      if (implKind == MethodHandleInfo.REF_newInvokeSpecial) {
        visitTypeInsn(NEW, implMethodClassName);
        visitInsn(DUP);
      }
      for (int i = 0; i < argNames.length; i++) {
        visitVarInsn(ALOAD, 0);
        visitFieldInsn(GETFIELD, lambdaClassName, argNames[i], argDescs[i]);
      }

      convertArgumentTypes(methodType);

      // Invoke the method we want to forward to
      visitMethodInsn(invocationOpcode(), implMethodClassName,
          implMethodName, implMethodDesc,
          implDefiningClass.isInterface());

      // Convert the return value (if any) and return it
      // Note: if adapting from non-void to void, the 'return'
      // instruction will pop the unneeded result
      Class<?> samReturnClass = methodType.returnType();
      convertType(implMethodReturnClass, samReturnClass, samReturnClass);
      visitInsn(getReturnOpcode(samReturnClass));
      // Maxs computed by ClassWriter.COMPUTE_MAXS,these arguments ignored
      visitMaxs(-1, -1);
      visitEnd();
    }

    private void convertArgumentTypes(MethodType samType) {
      int lvIndex = 0;
      boolean samIncludesReceiver = implIsInstanceMethod &&
          invokedType.parameterCount() == 0;
      int samReceiverLength = samIncludesReceiver ? 1 : 0;
      if (samIncludesReceiver) {
        // push receiver
        Class<?> rcvrType = samType.parameterType(0);
        visitVarInsn(getLoadOpcode(rcvrType), lvIndex + 1);
        lvIndex += getParameterSize(rcvrType);
        convertType(rcvrType, implDefiningClass, instantiatedMethodType.parameterType(0));
      }
      int samParametersLength = samType.parameterCount();
      int argOffset = implMethodType.parameterCount() - samParametersLength;
      for (int i = samReceiverLength; i < samParametersLength; i++) {
        Class<?> argType = samType.parameterType(i);
        visitVarInsn(getLoadOpcode(argType), lvIndex + 1);
        lvIndex += getParameterSize(argType);
        convertType(argType, implMethodType.parameterType(argOffset + i),
            instantiatedMethodType.parameterType(i));
      }
    }

    private int invocationOpcode() throws InternalError {
      switch (implKind) {
        case MethodHandleInfo.REF_invokeStatic:
          return INVOKESTATIC;
        case MethodHandleInfo.REF_newInvokeSpecial:
          return INVOKESPECIAL;
        case MethodHandleInfo.REF_invokeVirtual:
          return INVOKEVIRTUAL;
        case MethodHandleInfo.REF_invokeInterface:
          return INVOKEINTERFACE;
        case MethodHandleInfo.REF_invokeSpecial:
          return INVOKESPECIAL;
        default:
          throw new InternalError("Unexpected invocation kind: " + implKind);
      }
    }
  }

  static int getParameterSize(Class<?> c) {
    if (c == Void.TYPE) {
      return 0;
    } else if (c == Long.TYPE || c == Double.TYPE) {
      return 2;
    }
    return 1;
  }

  static int getLoadOpcode(Class<?> c) {
    if (c == Void.TYPE) {
      throw new InternalError("Unexpected void type of load opcode");
    }
    return ILOAD + getOpcodeOffset(c);
  }

  static int getReturnOpcode(Class<?> c) {
    if (c == Void.TYPE) {
      return RETURN;
    }
    return IRETURN + getOpcodeOffset(c);
  }

  private static int getOpcodeOffset(Class<?> c) {
    if (c.isPrimitive()) {
      if (c == Long.TYPE) {
        return 1;
      } else if (c == Float.TYPE) {
        return 2;
      } else if (c == Double.TYPE) {
        return 3;
      }
      return 0;
    } else {
      return 4;
    }
  }

}
